package com.xyy.saas.payment.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.pingplusplus.Pingpp;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.WechatPayUploadHttpPost;
import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import com.xyy.saas.payment.exception.PaymentException;
import com.xyy.util.StringUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.xml.sax.InputSource;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.xml.bind.DatatypeConverter;
import java.io.*;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;


/**
 * @Author: wuqiongda
 * @Date: 2019/12/16 13:36
 */
public class WxUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(WxUtil.class);

    /**
     * 随机数生成
     * @return
     */
    public static String getNonceStr() {
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 15; i++) {
            int number = ThreadLocalRandom.current().nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }

    //map转xml 加上签名信息
    public static String map2Xml(Map<String, Object> map, String businessOrderType, boolean isNewMerchant) throws Exception {
        StringBuffer sb = new StringBuffer();
        StringBuilder sb2 = new StringBuilder();
        sb2.append("<xml>");
        for (String key : map.keySet()) {
            sb.append(key);
            sb.append('=');
            sb.append(map.get(key));
            sb.append('&');
            // sb2是用来做请求的xml参数
            sb2.append("<" + key + ">");
            sb2.append(map.get(key));
            sb2.append("</" + key + ">");
        }
        if (isNewMerchant){
            sb.append("key=" + ApolloUtil.getProperty(businessOrderType+".wx.newkey"));
        }else {
            sb.append("key=" + ApolloUtil.getProperty(businessOrderType+".wx.key"));
        }
        String sign = DigestUtils.md5Hex(getContentBytes(sb.toString(),"utf-8")).toUpperCase();
        sb2.append("<sign>");
        sb2.append(sign);
        sb2.append("</sign>");
        sb2.append("</xml>");
        return sb2.toString();
    }

    //map转xml 加上签名信息
    public static String directMap2Xml(Map<String, Object> map, String apikey) throws Exception {
        StringBuffer sb = new StringBuffer();
        StringBuilder sb2 = new StringBuilder();
        sb2.append("<xml>");
        for (String key : map.keySet()) {
            sb.append(key);
            sb.append('=');
            sb.append(map.get(key));
            sb.append('&');
            // sb2是用来做请求的xml参数
            sb2.append("<" + key + ">");
            sb2.append(map.get(key));
            sb2.append("</" + key + ">");
        }
        sb.append("key=" + apikey);
        String sign = DigestUtils.md5Hex(getContentBytes(sb.toString(),"utf-8")).toUpperCase();
        sb2.append("<sign>");
        sb2.append(sign);
        sb2.append("</sign>");
        sb2.append("</xml>");
        return sb2.toString();
    }


    /**
     * MD5加密
     * @param content
     * @param charset
     * @return
     */
    public static byte[] getContentBytes(String content, String charset) {
        if (charset == null || "".equals(charset)) {
            return content.getBytes();
        }
        try {
            return content.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("MD5签名过程中出现错误,指定的编码集不对,您目前指定的编码集是:" + charset);
        }
    }

    public static SortedMap<String, String> doXMLParse(String strXML) throws DocumentException {
        SortedMap<String, String> smap = new TreeMap<String, String>();
        Document doc = parseText(strXML);
        if(doc == null) {
            return smap;
        }
        Element root = doc.getRootElement();
        for (Iterator iterator = root.elementIterator(); iterator.hasNext();) {
            Element e = (Element) iterator.next();
            smap.put(e.getName(), e.getText());
        }
        return smap;
    }

    public static Document parseText(String text) throws DocumentException {
        Document result = null;
        SAXReader reader = new SAXReader();
        try {
            reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
            reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);

            String encoding = getEncoding(text);
            InputSource source = new InputSource(new StringReader(text));
            source.setEncoding(encoding);
            result = reader.read(source);
            if(result.getXMLEncoding() == null) {
                result.setXMLEncoding(encoding);
            }

            return result;
        } catch (Exception e) {
            LOGGER.error("parseText fail. text:{}",text,e);
        }
        return result;
    }

    private static String getEncoding(String text) {
        String result = null;
        String xml = text.trim();
        if(xml.startsWith("<?xml")) {
            int end = xml.indexOf("?>");
            String sub = xml.substring(0, end);
            StringTokenizer tokens = new StringTokenizer(sub, " =\"'");

            while(tokens.hasMoreTokens()) {
                String token = tokens.nextToken();
                if("encoding".equals(token)) {
                    if(tokens.hasMoreTokens()) {
                        result = tokens.nextToken();
                    }
                    break;
                }
            }
        }

        return result;
    }

    public static String generateSign(Map<String, Object> map, String businessOrderType, boolean isNewMerchant) throws Exception {
        StringBuffer sb = new StringBuffer();
        for (String key : map.keySet()) {
            sb.append(key);
            sb.append('=');
            sb.append(map.get(key));
            sb.append('&');
        }
        if (isNewMerchant){
            sb.append("key=" + ApolloUtil.getProperty(businessOrderType+".wx.newkey"));
        }else {
            sb.append("key=" + ApolloUtil.getProperty(businessOrderType+".wx.key"));
        }
        String sign = DigestUtils.md5Hex(getContentBytes(sb.toString(),null)).toUpperCase();
        return sign;
    }

    public static String getSign(Map<String, Object> map,String apiKey){
        StringBuffer sb = new StringBuffer();
        for (String key : map.keySet()) {
            sb.append(key);
            sb.append('=');
            sb.append(map.get(key));
            sb.append('&');
        }
        sb.append("key=" + apiKey);
        String sign = DigestUtils.md5Hex(getContentBytes(sb.toString(),"utf-8")).toUpperCase();
        return sign;
    }


    public static SSLConnectionSocketFactory initKeyStore(String wxMchId)
            throws KeyStoreException, FileNotFoundException, IOException,
            NoSuchAlgorithmException,
            CertificateException, KeyManagementException, UnrecoverableKeyException {
        KeyStore keyStore  = KeyStore.getInstance("PKCS12");
        org.springframework.core.io.Resource fileRource = null;
        if ("1566654551".equals(wxMchId)|| StringUtil.isEmpty(wxMchId)){
            wxMchId = "1566654551";
            fileRource = new ClassPathResource("yikuaiqian_doc_oldapiclient_cert.p12");
        }
        if ("1578283921".equals(wxMchId)){
            fileRource = new ClassPathResource("yikuaiqian_doc_newapiclient_cert.p12");
        }
        if (fileRource == null) {
            LOGGER.error("initKeyStore fail file not found");
            return null;
        }
        InputStream inputStream = fileRource.getInputStream();
        try {
            keyStore.load(inputStream, wxMchId.toCharArray());
        } finally {
            inputStream.close();
        }
        // Trust own CA and all self-signed certs
        SSLContext sslcontext = SSLContexts.custom()
                .loadKeyMaterial(keyStore,wxMchId.toCharArray())
                .build();
        // Allow TLSv1 protocol only
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext,
                new String[] { "TLSv1" },
                null,
                SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        return sslsf;
    }

    public static SSLConnectionSocketFactory initKeyStoreByPem(String apiCert, String privateKey, String wxMchId)
            throws KeyStoreException, IOException, NoSuchAlgorithmException,
            CertificateException, KeyManagementException, UnrecoverableKeyException, InvalidKeySpecException {

        byte[] certBytes = parseDERFromPEM(apiCert, "-----BEGIN CERTIFICATE-----\n", "\n-----END CERTIFICATE-----");
        byte[] keyBytes = parseDERFromPEM(privateKey, "-----BEGIN PRIVATE KEY-----\n", "\n-----END PRIVATE KEY-----");
        X509Certificate cert = generateCertificateFromDER(certBytes);
        RSAPrivateKey key  = generatePrivateKeyFromDER(keyBytes);

        KeyStore keystore = KeyStore.getInstance("JKS");
        keystore.load(null);
        keystore.setCertificateEntry("cert-alias", cert);
        keystore.setKeyEntry("key-alias", key, wxMchId.toCharArray(), new java.security.cert.Certificate[] {cert});

        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(keystore, wxMchId.toCharArray());

        KeyManager[] km = kmf.getKeyManagers();
        // Trust own CA and all self-signed certs
        SSLContext sslcontext = SSLContext.getInstance("TLS");
        sslcontext.init(km, null, null);
        // Allow TLSv1 protocol only
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext,
                new String[] { "TLSv1" },
                null,
                SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        return sslsf;
    }

    public static PrivateKey getPrivateKeyFromPEM(String PEMEncodedPrivateKey) {
        PEMEncodedPrivateKey = PEMEncodedPrivateKey
                .replaceAll("(-+BEGIN (RSA )?PRIVATE KEY-+\\r?\\n|-+END (RSA )?PRIVATE KEY-+\\r?\\n?)", "");
        byte[] privateKeyBytes = Base64.decodeBase64(PEMEncodedPrivateKey);

        try {
            return generatePrivateKeyWithPKCS8(privateKeyBytes);
        } catch (InvalidKeySpecException e) {
            if (Pingpp.DEBUG) {
                e.printStackTrace();
            }
            return null;
        }
    }

    public static PrivateKey generatePrivateKeyWithPKCS8(byte[] privateKeyBytes)
            throws InvalidKeySpecException {
        try {
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
            KeyFactory kf = KeyFactory.getInstance("RSA");
            return kf.generatePrivate(keySpec);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    protected static byte[] parseDERFromPEM(String data, String beginDelimiter, String endDelimiter) {
        String var1 = data.replaceAll(beginDelimiter, "");
        String var2 = var1.replaceAll(endDelimiter, "");
        return DatatypeConverter.parseBase64Binary(var2);
    }

    protected static RSAPrivateKey generatePrivateKeyFromDER(byte[] keyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);

        KeyFactory factory = KeyFactory.getInstance("RSA");

        return (RSAPrivateKey)factory.generatePrivate(spec);
    }

    protected static X509Certificate generateCertificateFromDER(byte[] certBytes) throws CertificateException {
        CertificateFactory factory = CertificateFactory.getInstance("X.509");

        return (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes));
    }


    // =========== 微信平台证书相关 ===============
    public static WxUtil.WxCertEntity buildWxCert(String merchantId, String apiV3Key, String certSerial, String privateKey) {
        return new WxUtil().buildWxCertEntity(merchantId, apiV3Key, certSerial, privateKey);
    }

    // 微信平台证书相关实体类 及相关操作
    public static class WxCertEntity {
        private String merchantId;
        private String apiV3Key;
        private String certSerial;
        private String privateKey;
        private PrivateKey merchantPrivateKey = null;
        private AutoUpdateCertificatesVerifier verifier = null;
        private X509Certificate certificate;

        public WxCertEntity(String merchantId, String apiV3Key, String certSerial, String privateKey) {
            this.merchantId = merchantId;
            this.apiV3Key = apiV3Key;
            this.certSerial = certSerial;
            this.privateKey = privateKey;
        }

        public CloseableHttpClient buildWxHttpClient() {
            return buildWxHttpClient(true);
        }

        public CloseableHttpClient buildWxHttpClient(boolean needValidator) {
            try {
                if(needValidator) {
                    return WechatPayHttpClientBuilder.create()
                            .withMerchant(merchantId, certSerial, merchantPrivateKey)
                            .withValidator(new WechatPay2Validator(verifier))
                            .build();
                }
                else {
                    return WechatPayHttpClientBuilder.create()
                            .withMerchant(merchantId, certSerial, merchantPrivateKey)
                            .withValidator(response -> true)
                            .build();
                }
            }
            catch(Exception e) {
                LOGGER.error("WxUtil#buildWxHttpClient error. merchantId:{},apiV3Key:{},certSerial:{},privateKey:{},e:{}", merchantId, apiV3Key, certSerial, privateKey, e);
            }

            return null;
        }

        public String wxHttpPostRequest(String url,String json) throws Exception {
            return wxHttpPostRequest(url,json, new HashMap<>(2));
        }

        public String wxHttpPostRequest(String url,String json, Map<String, String> header) throws Exception {
            HttpPost httpPost = new HttpPost(url);

            header.forEach((key, value) -> {
                httpPost.addHeader(key, value);
            });

            httpPost.setEntity(new StringEntity(json, "UTF-8"));
            CloseableHttpResponse response = buildWxHttpClient().execute(httpPost);
            int statusCode = response.getStatusLine().getStatusCode();
            String body = EntityUtils.toString(response.getEntity());

            if(statusCode != 200) {
                LOGGER.warn("WxUtil#wxHttpPostRequest request failed, statusCode = " + statusCode + ",body = " + body);
                return StringUtils.EMPTY;
            }

            return EntityUtils.toString(response.getEntity());
        }

        public CloseableHttpResponse wxHttpPostRequestRetResp(String url,String json) throws Exception {
            return wxHttpPostRequestRetResp(url, json, new HashMap<>(2));
        }

        public CloseableHttpResponse wxHttpPostRequestRetResp(String url,String json, Map<String, String> header) throws Exception {
            HttpPost httpPost = new HttpPost(url);

            header.forEach((key, value) -> {
                httpPost.addHeader(key, value);
            });

            httpPost.setEntity(new StringEntity(json, "UTF-8"));
            return buildWxHttpClient().execute(httpPost);
        }

        public CloseableHttpResponse wxHttpPostRequestRetResp(String url,Map<String, String> paramMap, Map<String, String> header) throws Exception {
            HttpPost httpPost = new HttpPost(url);

            header.forEach((key, value) -> {
                httpPost.addHeader(key, value);
            });

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectMapper objectMapper = new ObjectMapper();
            ObjectNode rootNode = objectMapper.createObjectNode();

            paramMap.forEach((key, value) -> {
                rootNode.put(key, value);
            });

            objectMapper.writeValue(bos, rootNode);
            httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
            return buildWxHttpClient().execute(httpPost);
        }

        public String wxHttpGetRequest(String url) throws Exception {
            return wxHttpGetRequest(url, new HashMap<>(2));
        }

        public String wxHttpGetRequest(String url, Map<String, String> header) throws Exception {
            HttpGet httpGet = new HttpGet(url);

            header.forEach((key, value) -> {
                httpGet.addHeader(key, value);
            });

            CloseableHttpResponse response = buildWxHttpClient().execute(httpGet);
            int statusCode = response.getStatusLine().getStatusCode();
            String body = EntityUtils.toString(response.getEntity());

            if(statusCode != 200) {
                LOGGER.warn("WxUtil#wxHttpGetRequest request failed, statusCode = " + statusCode + ",body = " + body);
                return StringUtils.EMPTY;
            }

            return EntityUtils.toString(response.getEntity());
        }

        public InputStream wxHttpGetStreamRequest(String url, Map<String, String> header, boolean needValidator) throws Exception {
            HttpGet httpGet = new HttpGet(url);

            header.forEach((key, value) -> {
                httpGet.addHeader(key, value);
            });

            CloseableHttpResponse response = buildWxHttpClient(needValidator).execute(httpGet);
            int statusCode = response.getStatusLine().getStatusCode();

            if(statusCode != 200) {
                LOGGER.warn("WxUtil#wxHttpGetRequest request failed, statusCode = " + statusCode + ",url = " + url);
                throw new PaymentException("WxUtil#wxHttpGetRequest request failed, statusCode = " + statusCode + ",url = " + url);
            }

            return response.getEntity().getContent();
        }

        /**
         * 获取平台的证书序列号
         * @return
         */
        public String getCertSerialNo() {
            try {
                Map<String, String> header = new HashMap<>();
                header.put("Accept","application/json");
                header.put("Content-type","application/json; charset=utf-8");
                header.put("Wechatpay-Serial", certSerial);
                Map<String, List> stringStringMap = JSONUtils.parse2Map(wxHttpGetRequest("https://api.mch.weixin.qq.com/v3/certificates", header), String.class, List.class);
                Map data = (Map) stringStringMap.get("data").get(0);
                return (String) data.get("serial_no");
            }
            catch(Exception e) {
                LOGGER.error("WxUtil#getCertSerialNo get serial no fail. e:" + e.getMessage(), e);
            }

            return StringUtils.EMPTY;
        }

        /**
         * 上传网络图片到微信
         * @param imgUrl
         * @param uploadUrl
         * @return
         */
        public String uploadImg2Wx(String imgUrl, String uploadUrl) {
            try(InputStream ins1 = PicUtil.getImageStream(imgUrl); InputStream ins2 = PicUtil.getImageStream(imgUrl)) {
                String sha256 = DigestUtils.sha256Hex(ins1);
                URI uri = new URI(uploadUrl);
                String fileName = imgUrl.substring(imgUrl.lastIndexOf('/')+1);
                HttpPost request = new WechatPayUploadHttpPost.Builder(uri)
                        .withImage(fileName, sha256, ins2)
                        .build();
                HttpEntity result = buildWxHttpClient().execute(request).getEntity();
                Map<String, String> media = JSONUtils.parse2Map(EntityUtils.toString(result), String.class, String.class);
                return media.get("media_id");
            }
            catch(Exception e) {
                LOGGER.error("WxUtil#uploadImg2Wx error. imgUrl:{}, uploadUrl:{}, e", imgUrl, uploadUrl, e);
            }

            return StringUtils.EMPTY;
        }

        /**
         * 微信敏感信息加密
         * @param message
         * @return
         * @throws IllegalBlockSizeException
         */
        public String encryptOAEP(String message) throws IllegalBlockSizeException {
            try {
                Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
                cipher.init(1, certificate.getPublicKey());
                byte[] data = message.getBytes(StandardCharsets.UTF_8);
                byte[] ciphertext = cipher.doFinal(data);
                return java.util.Base64.getEncoder().encodeToString(ciphertext);
            }
            catch(NoSuchPaddingException | NoSuchAlgorithmException var5) {
                throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", var5);
            }
            catch(InvalidKeyException var6) {
                throw new IllegalArgumentException("无效的证书", var6);
            }
            catch(BadPaddingException | IllegalBlockSizeException var7) {
                throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
            }
        }

        /**
         * 微信敏感信息解密
         * @param ciphertext
         * @return
         * @throws BadPaddingException
         */
        public String decryptOAEP(String ciphertext) throws BadPaddingException {
            try {
                Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
                cipher.init(2, merchantPrivateKey);
                byte[] data = java.util.Base64.getDecoder().decode(ciphertext);
                return new String(cipher.doFinal(data), StandardCharsets.UTF_8);
            }
            catch(NoSuchAlgorithmException | NoSuchPaddingException var4) {
                throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", var4);
            }
            catch(InvalidKeyException var5) {
                throw new IllegalArgumentException("无效的私钥", var5);
            }
            catch(IllegalBlockSizeException | BadPaddingException var6) {
                throw new BadPaddingException("解密失败");
            }
        }
    }

    public WxCertEntity buildWxCertEntity(String merchantId, String apiV3Key, String certSerial, String privateKey) {
        WxCertEntity wxEntity = new WxCertEntity(merchantId, apiV3Key, certSerial, privateKey);

        try {
            wxEntity.merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
            //不需要传入微信支付证书了
            wxEntity.verifier = new AutoUpdateCertificatesVerifier(
                    new WechatPay2Credentials(merchantId, new PrivateKeySigner(certSerial, wxEntity.merchantPrivateKey)),
                    apiV3Key.getBytes("utf-8"));
            wxEntity.certificate = wxEntity.verifier.getValidCertificate();
        }
        catch(Exception e) {
            LOGGER.error("WxUtil#buildWxCertEntity error. merchantId:{},apiV3Key:{},certSerial:{},privateKey:{},e:{}", merchantId, apiV3Key, certSerial, privateKey, e);
        }

        return wxEntity;
    }

}
